package com.ease.framework {

import com.ease.util.ObjectEvent;
import com.ease.util.ObjectFilter;
import com.ease.util.ReflectionUtil;
import com.ease.util.collections.TypeBasedObjectCache;
import com.ease.util.gc.SharedReference;

import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;


/**
 * @private
 *
 * An object directory that creates a new instance for the looked up object filter/spec if
 * an already existing object matching the spec is not found.
 */
public class FactoryBasedObjectDirectory implements IObjectDirectory
{
   private static const TAGPARAM_INSTANCEID_KEY:String =
         Constants.TAGPARAM_INSTANCEID_KEY;
   private var _objectById:Dictionary = new Dictionary();
   private var _objectsByTypeCache:TypeBasedObjectCache;
   private var _idByObject:Dictionary = new Dictionary();
   private var _dispatcherRef:SharedReference = SharedReference.newReference(EventBus);

   /**
    * Initializes an instance of this class.
    *
    * @param dispatcher
    *    The event dispatcher on which the addition (or creation) and removal of objects
    *    is notified using ObjectEvent.
    */
   public function FactoryBasedObjectDirectory( dispatcher:IEventDispatcher = null ) {
      this.dispatcher = dispatcher;
      _objectsByTypeCache = new TypeBasedObjectCache();
      _objectsByTypeCache.usesConcreteTypeOnly = false;
   }

   /**
    * Adds the given object to the directory, optionally against a string id.
    *
    * @param object
    *    The object to be registered with this directory.
    *
    * @param id
    *    An optional string id that identifies the object. If no id is provided, the
    *    object's (class or interface) type can be used for identification, e.g. when
    *    looking up the object from the directory.
    */
   public final function addObject( object:Object, id:String = null ):void {
      if (object == null) {
         return;
      }
      if (id != null) {
         _objectById[id] = object;
         _idByObject[object] = id;
      }
      _objectsByTypeCache.add( object );
      notifyEvent( ObjectEvent.newAddition( object ) );
   }

   /**
    * Removes the given object from the directory.
    *
    * @param object
    *    The object to remove.
    */
   public final function removeObject( object:Object ):void {
      if (object == null) {
         return;
      }
      var id:String = _idByObject[object];
      if (id != null) {
         delete _objectById[id];
         delete _idByObject[object];
      }
      _objectsByTypeCache.remove( object );
      notifyEvent( ObjectEvent.newRemoval( object ) );
   }

   /**
    * Looks up an object in the directory by the given id or type. When looking up by
    * type, creates a new instance if possible and if an instance doesn't already exist.
    *
    * @param id
    *    The string identifying the object to be looked up. It would have to be the same
    *    id that was provided when originally registering the object in order for this to
    *    be a successful lookup.
    *
    * @param policy
    *    The lifetime or creation policy for the requested object.
    *
    * @see ObjectFilter#policy
    */
   public final function getObject( id:String, type:Class = null,
                                    moduleReference:* = null,
                                    policy:String = null ):Object {
      if (id != null) {
         return _objectById[id];
      }
      if (! type) {
         return null;
      }
      var object:Object = createOrLookupObjectFor( type, moduleReference, policy );
      return object;
   }

   /**
    * Looks up a object in the directory by the given object type.
    *
    * @param type
    *    The type can be either the actual class of the object, or a base class or a
    *    compatible interface.
    */
   public final function getObjectsByType( type:Class ):Array {
      var objects:Array = _objectsByTypeCache.getObjects( type );
      return objects;
   }

   /**
    * Looks up existing or creates new objects that fit the given spec.
    *
    * @see IObjectDirectory#getObjects()
    */
   public final function getObjects( spec:ObjectFilter, callback:Function,
                                     moduleReference:* = null,
                                     currentObjectSet:Array/*?*/ = null ):void {
      // Note: It'd be nice to find the intersection between currentObjectSet and the
      // object set computed here, and place the common items at the front of the array
      // returned from here (in the same order as they are in currentObjectSet), but doing
      // so hasn't been necessary so far.

      getObjects_Internal( spec, spec.policy, callback, moduleReference );
   }

   /**
    * Looks up existing objects that fit the given spec.
    *
    * @see #getObjects()
    */
   public final function getExistingObjects( spec:ObjectFilter ):Array/*?*/ {
      var objects:Array/*?*/ =
            getObjects_Internal( spec, Constants.TAGPARAM_POLICY_LOOKUP_ONLY );
      return objects;
   }

   /**
    * Looks up existing or creates new objects that fit the given spec, and as per the
    * given policy (which can be "createnew", "lookuponly", etc.). Invokes the callback
    * with the result if a callback is provided; also returns the result.
    */
   private function getObjects_Internal( spec:ObjectFilter, policy:String,
                                         callback:Function = null,
                                         moduleReference:* = null ):Array/*?*/ {
      if (containsUnrecognizedMetaPropertyFilters( spec )) {
         // This spec has some meta-property filter that we don't recognize, hence can't
         // filter on. In other words, we can't ensure that our objects satisfy this
         // filter. Since the object provider (i.e., this directory) is supposed to
         // ascertain the meta-property filter, we have to return empty from here.
         if (callback != null) {
            callback( [] );
         }
         return [];
      }
      var objects:Array/*?*/ = [];
      var object:Object;
      var instanceId:String = getInstanceIdIfAny( spec );
      if (instanceId != null) {
         object = _objectById[instanceId];
         if (object != null) {
            objects = [object];
         }
      } else {
         objects = getObjectsByType( spec.type );
      }
      objects = spec.filter( objects );
      if (objects.length == 0 && ! isLookup( policy )) {
         object = createOrLookupObjectFor( spec.type, moduleReference, policy );
         var filterSatisfied:Boolean = spec.setFilteredPropertiesOn( object );
         if (filterSatisfied) {
            objects = [object];
         }
      }
      if (callback != null) {
         callback( objects );
      }
      return objects;
   }

   /** @inheritDoc */
   public final function evaluateMetaFilter( spec:ObjectFilter, object:* ):Boolean {
      var instanceId:String = getInstanceIdIfAny( spec );
      if (instanceId == null) {
         // Instance id is the only meta-filter that this class filters on, hence, if no
         // such meta-filter is found, the object is deemed to be passing the filter.
         return true;
      }
      var cachedObj:* = _objectById[instanceId];
      if (cachedObj && cachedObj != object) {
         // The passed-in object is different from the (cached) object which is registered
         // as having the given id.
         return false;
      }
      return true;
   }

   private function containsUnrecognizedMetaPropertyFilters( spec:ObjectFilter ):Boolean {
      if (spec == null || spec.metaPropertyFilterCount == 0) {
         return false;
      }
      if (spec.metaPropertyFilterCount > 1) {
         return true;
      }
      if (spec.containsMetaPropertyFilterFor( TAGPARAM_INSTANCEID_KEY )) {
         return false;
      }
      return true;
   }

   private function getInstanceIdIfAny( spec:ObjectFilter ):String {
      if (! spec.containsMetaPropertyFilterFor( TAGPARAM_INSTANCEID_KEY )) {
         return null;
      }
      var instanceId:String =
            spec.getMetaPropertyValue( TAGPARAM_INSTANCEID_KEY ) as String;
      return instanceId;
   }

   /**
    * @param policy
    *    Creation or lifetime policy for the requested object.
    */
   private function
   createOrLookupObjectFor( type:Class, moduleReference:* = null,
                            policy:String = null ):Object {
      // Look for existing objects only if "createnew" is not specified.
      if (! isCreateNew( policy )) {
         var objects:Array = getObjectsByType( type );
         if (objects.length > 0) {
            return objects[0];
         }
      }
      // Don't create a new object if "lookuponly" is specified.
      if (isLookup( policy )) {
         return null;
      }
      var typeName:String = getQualifiedClassName( type );
      // create an object only if it's not a primitive type or plain Object type
      if (ReflectionUtil.isSimpleType( typeName )) {
         return null;
      }
      var objectType:Class = type;
      var object:Object;
      try {
         object = new objectType;
      } catch (err:Error) {
         // ignore
      }
      if (object == null) {
         // see if the type is an appropriately tagged interface
         var objectClassName:String =
               ReflectionUtil.getClassTagValue( objectType, "DefaultImplementation" );
         if (objectClassName == null) {
            // no default impl specified, so nothing to do
            trace( "No DefaultImplementation specified for: " + typeName );
            return null;
         }
         object = ReflectionUtil.newInstance( objectClassName, moduleReference );
      }
      if (object == null) {
         trace( "Couldn't create an instance of class (default ctor may be missing): " +
                typeName );
         return null;
      }
      if (policy == null) {
         // If no policy is specified at the call-site, use the default defined by the
         // class definition.
         policy = ReflectionUtil.getClassTagValue( object, "LifetimePolicy" );
      }
      // add the object to the directory if its lifetime policy is "keepalive"
      if (isKeepAlive( policy )) {
         addObject( object );
      } else {
         notifyEvent( ObjectEvent.newAddition( object, true ) );
      }
      return object;
   }

   private function notifyEvent( event:Event ):void {
      if (this.dispatcher == null) {
         return;
      }
      this.dispatcher.dispatchEvent( event );
   }

   private function isLookup( policy:String ):Boolean {
      return policy == "lookuponly" || policy == "lookupOnly";
   }

   private function isCreateNew( policy:String ):Boolean {
      return policy == "createnew" || policy == "createNew";
   }

   private function isKeepAlive( policy:String ):Boolean {
      return policy == "keepalive" || policy == "keepAlive";
   }

   //------------------------------------------------------------------------------------
   // Instance properties
   // The property provider used avoids directly referencing the global instance.
   // This is done to simplify the object graph for Memory Profiler.

   private function get dispatcher():IEventDispatcher {
      return _dispatcherRef.object;
   }

   private function set dispatcher(value:IEventDispatcher):void {
      _dispatcherRef.object = value;
   }
} // class

}